--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit f5d263ee31037b178da55f117f748a9bff892f7b
Parents : ac81f07
Author : Mark Qvist <mark@unsigned.io>
Date : 2021-05-04T15:10:02+02:00
Included additional-urwid-widgets by AFoeee
Changes
11 files changed, 1250 insertions(+), 0 deletions(-)
Diff
diff --git a/nomadnet/vendor/additional_urwid_widgets/LICENSE b/nomadnet/vendor/additional_urwid_widgets/LICENSE
new file mode 100644
index 0000000..0bf84bc
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 AFoeee
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/nomadnet/vendor/additional_urwid_widgets/__init__.py b/nomadnet/vendor/additional_urwid_widgets/__init__.py
new file mode 100644
index 0000000..d7b0867
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/__init__.py
@@ -0,0 +1,10 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+from .assisting_modules.modifier_key import MODIFIER_KEY
+from .widgets.date_picker import DatePicker
+from .widgets.indicative_listbox import IndicativeListBox
+from .widgets.integer_picker import IntegerPicker
+from .widgets.message_dialog import MessageDialog
+from .widgets.selectable_row import SelectableRow
\ No newline at end of file
diff --git a/nomadnet/vendor/additional_urwid_widgets/assisting_modules/__init__.py b/nomadnet/vendor/additional_urwid_widgets/assisting_modules/__init__.py
new file mode 100644
index 0000000..b16fa58
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/assisting_modules/__init__.py
@@ -0,0 +1,2 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
diff --git a/nomadnet/vendor/additional_urwid_widgets/assisting_modules/modifier_key.py b/nomadnet/vendor/additional_urwid_widgets/assisting_modules/modifier_key.py
new file mode 100644
index 0000000..53c1e11
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/assisting_modules/modifier_key.py
@@ -0,0 +1,26 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+import enum
+
+
+class MODIFIER_KEY(enum.Enum):
+ """Represents modifier keys such as 'ctrl', 'shift' and so on.
+ Not every combination of modifier and input is useful."""
+
+ NONE = ""
+ SHIFT = "shift"
+ ALT = "meta"
+ CTRL = "ctrl"
+ SHIFT_ALT = "shift meta"
+ SHIFT_CTRL = "shift ctrl"
+ ALT_CTRL = "meta ctrl"
+ SHIFT_ALT_CTRL = "shift meta ctrl"
+
+ def append_to(self, text, separator=" "):
+ return (text + separator + self.value) if (self != self.__class__.NONE) else text
+
+ def prepend_to(self, text, separator=" "):
+ return (self.value + separator + text) if (self != self.__class__.NONE) else text
+
\ No newline at end of file
diff --git a/nomadnet/vendor/additional_urwid_widgets/assisting_modules/useful_functions.py b/nomadnet/vendor/additional_urwid_widgets/assisting_modules/useful_functions.py
new file mode 100644
index 0000000..946a2ab
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/assisting_modules/useful_functions.py
@@ -0,0 +1,47 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""A non-thematic collection of useful functions."""
+
+
+def recursively_replace(original, replacements, include_original_keys=False):
+ """Clones an iterable and recursively replaces specific values."""
+
+ # If this function would be called recursively, the parameters 'replacements' and 'include_original_keys' would have to be
+ # passed each time. Therefore, a helper function with a reduced parameter list is used for the recursion, which nevertheless
+ # can access the said parameters.
+
+ def _recursion_helper(obj):
+ #Determine if the object should be replaced. If it is not hashable, the search will throw a TypeError.
+ try:
+ if obj in replacements:
+ return replacements[obj]
+ except TypeError:
+ pass
+
+ # An iterable is recursively processed depending on its class.
+ if hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes, bytearray)):
+ if isinstance(obj, dict):
+ contents = {}
+
+ for key, val in obj.items():
+ new_key = _recursion_helper(key) if include_original_keys else key
+ new_val = _recursion_helper(val)
+
+ contents[new_key] = new_val
+
+ else:
+ contents = []
+
+ for element in obj:
+ new_element = _recursion_helper(element)
+
+ contents.append(new_element)
+
+ # Use the same class as the original.
+ return obj.__class__(contents)
+
+ # If it is not replaced and it is not an iterable, return it.
+ return obj
+
+ return _recursion_helper(original)
diff --git a/nomadnet/vendor/additional_urwid_widgets/widgets/__init__.py b/nomadnet/vendor/additional_urwid_widgets/widgets/__init__.py
new file mode 100644
index 0000000..b16fa58
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/widgets/__init__.py
@@ -0,0 +1,2 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
diff --git a/nomadnet/vendor/additional_urwid_widgets/widgets/date_picker.py b/nomadnet/vendor/additional_urwid_widgets/widgets/date_picker.py
new file mode 100644
index 0000000..8d53bd8
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/widgets/date_picker.py
@@ -0,0 +1,345 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+from ..assisting_modules.modifier_key import MODIFIER_KEY # pylint: disable=unused-import
+from ..assisting_modules.useful_functions import recursively_replace
+from .indicative_listbox import IndicativeListBox
+from .integer_picker import IntegerPicker
+from .selectable_row import SelectableRow
+
+import calendar
+import datetime
+import enum
+import urwid
+
+
+class DatePicker(urwid.WidgetWrap):
+ """Serves as a selector for dates."""
+
+ _TYPE_ERR_MSG = "type {} was expected for {}, but found: {}."
+ _VALUE_ERR_MSG = "unrecognized value: {}."
+
+ # These values are interpreted during the creation of the list items for the day picker.
+ class DAY_FORMAT(enum.Enum):
+ DAY_OF_MONTH = 1
+ DAY_OF_MONTH_TWO_DIGIT = 2
+ WEEKDAY = 3
+
+ # These values are interpreted during the initialization and define the arrangement of the pickers.
+ class PICKER(enum.Enum):
+ YEAR = 1
+ MONTH = 2
+ DAY = 3
+
+ # Specifies which dates are selectable.
+ class RANGE(enum.Enum):
+ ALL = 1
+ ONLY_PAST = 2
+ ONLY_FUTURE = 3
+
+ def __init__(self, initial_date=datetime.date.today(), *, date_range=RANGE.ALL, month_names=calendar.month_name, day_names=calendar.day_abbr,
+ day_format=(DAY_FORMAT.WEEKDAY, DAY_FORMAT.DAY_OF_MONTH), columns=(PICKER.DAY, PICKER.MONTH, PICKER.YEAR),
+ modifier_key=MODIFIER_KEY.CTRL, return_unused_navigation_input=False, year_jump_len=50, space_between=2,
+ min_width_each_picker=9, year_align="center", month_align="center", day_align="center", topBar_align="center",
+ topBar_endCovered_prop=("▲", None, None), topBar_endExposed_prop=("───", None, None), bottomBar_align="center",
+ bottomBar_endCovered_prop=("▼", None, None), bottomBar_endExposed_prop=("───", None, None), highlight_prop=(None, None)):
+ assert (type(date_range) == self.__class__.RANGE), self.__class__._TYPE_ERR_MSG.format("<enum 'DatePicker.RANGE'>",
+ "'date_range'",
+ type(date_range))
+
+ for df in day_format:
+ assert (type(df) == self.__class__.DAY_FORMAT), self.__class__._TYPE_ERR_MSG.format("<enum 'DatePicker.DAY_FORMAT'>",
+ "all elements of 'day_format'",
+ type(df))
+
+ # Relevant for 'RANGE.ONLY_PAST' and 'RANGE.ONLY_FUTURE' to limit the respective choices.
+ self._initial_year = initial_date.year
+ self._initial_month = initial_date.month
+ self._initial_day = initial_date.day
+
+ # The date pool can be limited, so that only past or future dates are selectable. The initial date is included in the
+ # pool.
+ self._date_range = date_range
+
+ # The presentation of months and weekdays can be changed by passing alternative values (e.g. abbreviations or numerical
+ # representations).
+ self._month_names = month_names
+ self._day_names = day_names
+
+ # Since there are different needs regarding the appearance of the day picker, an iterable can be passed, which allows a
+ # customization of the presentation.
+ self._day_format = day_format
+
+ # Specifies the text alignment of the individual pickers. The year alignment is passed directly to the year picker.
+ self._month_align = month_align
+ self._day_align = day_align
+
+ # The default style of a list entry. Since only one list entry will be visible at a time and there is also off focus
+ # highlighting, the normal value can be 'None' (it is never shown).
+ self._item_attr = (None, highlight_prop[0])
+
+ # A full list of months. (From 'January' to 'December'.)
+ self._month_list = self._generate_months()
+
+ # Set the respective values depending on the date range.
+ min_year = datetime.MINYEAR
+ max_year = datetime.MAXYEAR
+
+ month_position = self._initial_month - 1
+ day_position = self._initial_day - 1
+
+ if date_range == self.__class__.RANGE.ALL:
+ initial_month_list = self._month_list
+
+ elif date_range == self.__class__.RANGE.ONLY_PAST:
+ max_year = self._initial_year
+
+ # The months of the very last year may be shorten.
+ self._shortened_month_list = self._generate_months(end=self._initial_month)
+ initial_month_list = self._shortened_month_list
+
+ elif date_range == self.__class__.RANGE.ONLY_FUTURE:
+ min_year = self._initial_year
+
+ # The months of the very first year may be shorten.
+ self._shortened_month_list = self._generate_months(start=self._initial_month)
+ initial_month_list = self._shortened_month_list
+
+ # The list may not start at 1 but some other day of month, therefore use the first list item.
+ month_position = 0
+ day_position = 0
+
+ else:
+ raise ValueError(self.__class__._VALUE_ERR_MSG.format(date_range))
+
+ # Create pickers.
+ self._year_picker = IntegerPicker(self._initial_year,
+ min_v=min_year,
+ max_v=max_year,
+ jump_len=year_jump_len,
+ on_selection_change=self._year_has_changed,
+ modifier_key=modifier_key,
+ return_unused_navigation_input=return_unused_navigation_input,
+ topBar_align=topBar_align,
+ topBar_endCovered_prop=topBar_endCovered_prop,
+ topBar_endExposed_prop=topBar_endExposed_prop,
+ bottomBar_align=bottomBar_align,
+ bottomBar_endCovered_prop=bottomBar_endCovered_prop,
+ bottomBar_endExposed_prop=bottomBar_endExposed_prop,
+ display_syntax="{:04}",
+ display_align=year_align,
+ display_prop=highlight_prop)
+
+ self._month_picker = IndicativeListBox(initial_month_list,
+ position=month_position,
+ on_selection_change=self._month_has_changed,
+ modifier_key=modifier_key,
+ return_unused_navigation_input=return_unused_navigation_input,
+ topBar_align=topBar_align,
+ topBar_endCovered_prop=topBar_endCovered_prop,
+ topBar_endExposed_prop=topBar_endExposed_prop,
+ bottomBar_align=bottomBar_align,
+ bottomBar_endCovered_prop=bottomBar_endCovered_prop,
+ bottomBar_endExposed_prop=bottomBar_endExposed_prop,
+ highlight_offFocus=highlight_prop[1])
+
+ self._day_picker = IndicativeListBox(self._generate_days(self._initial_year, self._initial_month),
+ position=day_position,
+ modifier_key=modifier_key,
+ return_unused_navigation_input=return_unused_navigation_input,
+ topBar_align=topBar_align,
+ topBar_endCovered_prop=topBar_endCovered_prop,
+ topBar_endExposed_prop=topBar_endExposed_prop,
+ bottomBar_align=bottomBar_align,
+ bottomBar_endCovered_prop=bottomBar_endCovered_prop,
+ bottomBar_endExposed_prop=bottomBar_endExposed_prop,
+ highlight_offFocus=highlight_prop[1])
+
+ # To mimic a selection widget, 'IndicativeListbox' is wrapped in a 'urwid.BoxAdapter'. Since two rows are used for the bars,
+ # size 3 makes exactly one list item visible.
+ boxed_month_picker = urwid.BoxAdapter(self._month_picker, 3)
+ boxed_day_picker = urwid.BoxAdapter(self._day_picker, 3)
+
+ # Replace the 'DatePicker.PICKER' elements of the parameter 'columns' with the corresponding pickers.
+ replacements = {self.__class__.PICKER.YEAR : self._year_picker,
+ self.__class__.PICKER.MONTH : boxed_month_picker,
+ self.__class__.PICKER.DAY : boxed_day_picker}
+
+ columns = recursively_replace(columns, replacements)
+
+ # wrap 'urwid.Columns'
+ super().__init__(urwid.Columns(columns,
+ min_width=min_width_each_picker,
+ dividechars=space_between))
+
+ def __repr__(self):
+ return "{}(date='{}', date_range='{}', initial_date='{}-{:02}-{:02}', selected_date='{}')".format(self.__class__.__name__,
+ self.get_date(),
+ self._date_range,
+ self._initial_year,
+ self._initial_month,
+ self._initial_day,
+ self.get_date())
+
+ # The returned widget is used for all list entries.
+ def _generate_item(self, cols, *, align="center"):
+ return urwid.AttrMap(SelectableRow(cols, align=align),
+ self._item_attr[0],
+ self._item_attr[1])
+
+ def _generate_months(self, start=1, end=12):
+ months = []
+
+ for month in range(start, end+1):
+ item = self._generate_item([self._month_names[month]], align=self._month_align)
+
+ # Add a new instance variable which holds the numerical value. This makes it easier to get the displayed value.
+ item._numerical_value = month
+
+ months.append(item)
+
+ return months
+
+ def _generate_days(self, year, month):
+ start = 1
+ weekday, end = calendar.monthrange(year, month) # end is included in the range
+
+ # If the date range is 'ONLY_PAST', the last month does not end as usual but on the specified day.
+ if (self._date_range == self.__class__.RANGE.ONLY_PAST) and (year == self._initial_year) and (month == self._initial_month):
+ end = self._initial_day
+
+ # If the date range is 'ONLY_FUTURE', the first month does not start as usual but on the specified day.
+ elif (self._date_range == self.__class__.RANGE.ONLY_FUTURE) and (year == self._initial_year) and (month == self._initial_month):
+ start = self._initial_day
+ weekday = calendar.weekday(year, month, start)
+
+ days = []
+
+ for day in range(start, end+1):
+ cols = []
+
+ # The 'DatePicker.DAY_FORMAT' elements of the iterable are translated into columns of the day picker. This allows the
+ # presentation to be customized.
+ for df in self._day_format:
+ if df == self.__class__.DAY_FORMAT.DAY_OF_MONTH:
+ cols.append(str(day))
+
+ elif df == self.__class__.DAY_FORMAT.DAY_OF_MONTH_TWO_DIGIT:
+ cols.append(str(day).zfill(2))
+
+ elif df == self.__class__.DAY_FORMAT.WEEKDAY:
+ cols.append(self._day_names[weekday])
+
+ else:
+ raise ValueError(self.__class__._VALUE_ERR_MSG.format(df))
+
+ item = self._generate_item(cols, align=self._day_align)
+
+ # Add a new instance variable which holds the numerical value. This makes it easier to get the displayed value.
+ item._numerical_value = day
+
+ # Keeps track of the weekday.
+ weekday = (weekday + 1) if (weekday < 6) else 0
+
+ days.append(item)
+
+ return days
+
+ def _year_has_changed(self, previous_year, current_year):
+ month_position_before_change = self._month_picker.get_selected_position()
+
+ # Since there are no years in 'RANGE.ALL' that do not have the full month range, the body never needs to be changed after
+ # initialization.
+ if self._date_range != self.__class__.RANGE.ALL:
+ # 'None' stands for trying to keep the old value.
+ provisional_position = None
+
+ # If the previous year was the specified year, the shortened month range must be replaced by the complete one. If this
+ # shortened month range does not begin at 'January', then the difference must be taken into account.
+ if previous_year == self._initial_year:
+ if self._date_range == self.__class__.RANGE.ONLY_FUTURE:
+ provisional_position = self._month_picker.get_selected_item()._numerical_value - 1
+
+ self._month_picker.set_body(self._month_list,
+ alternative_position=provisional_position)
+
+ # If the specified year is selected, the full month range must be replaced with the shortened one.
+ elif current_year == self._initial_year:
+ if self._date_range == self.__class__.RANGE.ONLY_FUTURE:
+ provisional_position = month_position_before_change - (self._initial_month - 1)
+
+ self._month_picker.set_body(self._shortened_month_list,
+ alternative_position=provisional_position)
+
+ # Since the month has changed, the corresponding method is called.
+ self._month_has_changed(month_position_before_change,
+ self._month_picker.get_selected_position(),
+ previous_year=previous_year)
+
+ def _month_has_changed(self, previous_position, current_position, *, previous_year=None):
+ # 'None' stands for trying to keep the old value.
+ provisional_position = None
+
+ current_year = self._year_picker.get_value()
+
+ # Out of range values are changed by 'IndicativeListBox' to the nearest valid values.
+
+ # If the date range is 'ONLY_FUTURE', it may be that a month does not start on the first day. In this case, the value must
+ # be changed to reflect this difference.
+ if self._date_range == self.__class__.RANGE.ONLY_FUTURE:
+ # If the current or previous year is the specified year and the month was the specified month, the value has an offset
+ # of the specified day. Therefore the deposited numerical value is used. ('-1' because it's an index.)
+ if ((current_year == self._initial_year) or (previous_year == self._initial_year)) and (previous_position == 0):
+ provisional_position = self._day_picker.get_selected_item()._numerical_value - 1
+
+ # If the current year is the specified year and the current month is the specified month, the month begins not with
+ # the first day, but with the specified day.
+ elif (current_year == self._initial_year) and (current_position == 0):
+ provisional_position = self._day_picker.get_selected_position() - (self._initial_day - 1)
+
+ self._day_picker.set_body(self._generate_days(current_year,
+ self._month_picker.get_selected_item()._numerical_value),
+ alternative_position=provisional_position)
+
+ def get_date(self):
+ return datetime.date(self._year_picker.get_value(),
+ self._month_picker.get_selected_item()._numerical_value,
+ self._day_picker.get_selected_item()._numerical_value)
+
+ def set_date(self, date):
+ # If the date range is limited, test for the new limit.
+ if self._date_range != self.__class__.RANGE.ALL:
+ limit = datetime.date(self._initial_year, self._initial_month, self._initial_day)
+
+ if (self._date_range == self.__class__.RANGE.ONLY_PAST) and (date > limit):
+ raise ValueError("The passed date is outside the upper bound of the date range.")
+
+ elif (self._date_range == self.__class__.RANGE.ONLY_FUTURE) and (date < limit):
+ raise ValueError("The passed date is outside the lower bound of the date range.")
+
+ year = date.year
+ month = date.month
+ day = date.day
+
+ # Set the new values, if needed.
+ if year != self._year_picker.get_value():
+ self._year_picker.set_value(year)
+
+ if month != self._month_picker.get_selected_item()._numerical_value:
+ month_position = month - 1 # '-1' because it's an index.
+
+ if (self._date_range == self.__class__.RANGE.ONLY_FUTURE) and (year == self._initial_year):
+ # If the value should be negative, the behavior of 'IndicativeListBox' shows effect and position 0 is selected.
+ month_position = month_position - (self._initial_month - 1)
+
+ self._month_picker.select_item(month_position)
+
+ if day != self._day_picker.get_selected_item()._numerical_value:
+ day_position = day - 1 # '-1' because it's an index.
+
+ if (self._date_range == self.__class__.RANGE.ONLY_FUTURE) and (year == self._initial_year) and (month == self._initial_month):
+ day_position = day_position - (self._initial_day - 1)
+
+ self._day_picker.select_item(day_position)
+
\ No newline at end of file
diff --git a/nomadnet/vendor/additional_urwid_widgets/widgets/indicative_listbox.py b/nomadnet/vendor/additional_urwid_widgets/widgets/indicative_listbox.py
new file mode 100644
index 0000000..f49bd52
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/widgets/indicative_listbox.py
@@ -0,0 +1,433 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+from ..assisting_modules.modifier_key import MODIFIER_KEY # pylint: disable=unused-import
+
+import enum
+import random
+import urwid
+
+
+class IndicativeListBox(urwid.WidgetWrap):
+ """Adds two bars to a 'urwid.ListBox', that make it obvious that due to limited space only a part of the list items is displayed."""
+
+ _TYPE_ERR_MSG = "type {} was expected for {}, but found: {}."
+ _VALUE_ERR_MSG = "unrecognized value: {}."
+
+ # These values are translated by 'get_nearest_valid_position()' into the corresponding int values.
+ class POSITION(enum.Enum):
+ LAST = 1
+ MIDDLE = 2
+ RANDOM = 3
+
+ def __init__(self, body, *, position=0, on_selection_change=None, initialization_is_selection_change=False,
+ modifier_key=MODIFIER_KEY.NONE, return_unused_navigation_input=True, topBar_align="center",
+ topBar_endCovered_prop=("▲", None, None), topBar_endExposed_prop=("───", None, None), bottomBar_align="center",
+ bottomBar_endCovered_prop=("▼", None, None), bottomBar_endExposed_prop=("───", None, None), highlight_offFocus=None):
+ # If not already done, wrap each item of the body in an 'urwid.AttrMap'. This is necessary to enable off focus highlighting.
+ body[:] = [urwid.AttrMap(item, None) if not isinstance(item, urwid.AttrMap) else item
+ for item in body]
+
+ # The body of the 'urwid.Frame' is a 'urwid.ListBox'.
+ self._listbox = urwid.ListBox(body)
+
+ # Select the specified list position, or the nearest valid one.
+ nearest_valid_position = self._get_nearest_valid_position(position)
+
+ if nearest_valid_position is not None:
+ self._listbox.set_focus(nearest_valid_position)
+
+ # The bars are just 'urwid.Text' widgets.
+ self._top_bar = urwid.AttrMap(urwid.Text("", align=topBar_align),
+ None)
+
+ self._bottom_bar = urwid.AttrMap(urwid.Text("", align=bottomBar_align),
+ None)
+
+ # Wrap 'urwid.Frame'.
+ super().__init__(urwid.Frame(self._listbox,
+ header=self._top_bar,
+ footer=self._bottom_bar,
+ focus_part="body"))
+
+ # During the initialization of 'urwid.AttrMap', the value can be passed as non-dict. After initializing, its value can be
+ # manipulated by passing a dict. The dicts I create below will be used later to change the appearance of the bars.
+ self._topBar_endCovered_markup = topBar_endCovered_prop[0]
+ self._topBar_endCovered_focus = {None:topBar_endCovered_prop[1]}
+ self._topBar_endCovered_offFocus = {None:topBar_endCovered_prop[2]}
+
+ self._topBar_endExposed_markup = topBar_endExposed_prop[0]
+ self._topBar_endExposed_focus = {None:topBar_endExposed_prop[1]}
+ self._topBar_endExposed_offFocus = {None:topBar_endExposed_prop[2]}
+
+ self._bottomBar_endCovered_markup = bottomBar_endCovered_prop[0]
+ self._bottomBar_endCovered_focus = {None:bottomBar_endCovered_prop[1]}
+ self._bottomBar_endCovered_offFocus = {None:bottomBar_endCovered_prop[2]}
+
+ self._bottomBar_endExposed_markup = bottomBar_endExposed_prop[0]
+ self._bottomBar_endExposed_focus = {None:bottomBar_endExposed_prop[1]}
+ self._bottomBar_endExposed_offFocus = {None:bottomBar_endExposed_prop[2]}
+
+ # This is used to highlight the selected item when the widget does not have the focus.
+ self._highlight_offFocus = {None:highlight_offFocus}
+ self._last_focus_state = None
+ self._original_item_attr_map = None
+
+ # A hook which is triggered when the selection changes.
+ self.on_selection_change = on_selection_change
+
+ # Initialization can be seen as a special case of selection change. This is interesting in combination with the hook.
+ self._initialization_is_selection_change = initialization_is_selection_change
+
+ # 'MODIFIER_KEY' changes the behavior of the list box, so that it responds only to modified input. ('up' => 'ctrl up')
+ self._modifier_key = modifier_key
+
+ # If the list item at the top is selected and you navigate further upwards, the input is normally not swallowed by the
+ # list box, but passed on so that other widgets can interpret it. This may result in transferring the focus.
+ self._return_unused_navigation_input = return_unused_navigation_input
+
+ # Is 'on_selection_change' triggered during the initialization?
+ if initialization_is_selection_change and (on_selection_change is not None):
+ on_selection_change(None,
+ self.get_selected_position())
+
+ def __repr__(self):
+ return "{}(body='{}', position='{}')".format(self.__class__.__name__,
+ self.get_body(),
+ self.get_selected_position())
+
+ def render(self, size, focus=False):
+ just_maxcol = (size[0],)
+
+ # The size also includes the two bars, so subtract these.
+ modified_size = (size[0], # cols
+ size[1] - self._top_bar.rows(just_maxcol) - self._bottom_bar.rows(just_maxcol)) # rows
+
+ # Evaluates which ends are visible and calculates how many list entries are hidden above/below. This is a modified form
+ # of 'urwid.ListBox.ends_visible()'.
+ middle, top, bottom = self._listbox.calculate_visible(modified_size, focus=focus)
+
+ if middle is None: # empty list box
+ top_is_visible = True
+ bottom_is_visible = True
+
+ else:
+ top_is_visible = False
+ bottom_is_visible = False
+
+ trim_top, above = top
+ trim_bottom, below = bottom
+
+ if trim_top == 0:
+ pos = above[-1][1] if (len(above) != 0) else middle[2]
+
+ if self._listbox._body.get_prev(pos) == (None,None):
+ top_is_visible = True
+ else:
+ covered_above = pos
+ else:
+ covered_above = self.rearmost_position()
+
+ if trim_bottom == 0:
+ row_offset, _, pos, rows, _ = middle
+
+ row_offset += rows # Include the selected position.
+
+ for _, pos, rows in below: # 'pos' is overridden
+ row_offset += rows
+
+ if (row_offset < modified_size[1]) or (self._listbox._body.get_next(pos) == (None, None)):
+ bottom_is_visible = True
+ else:
+ covered_below = self.rearmost_position() - pos
+ else:
+ covered_below = self.rearmost_position()
+
+ # Changes the appearance of the bar at the top depending on whether the first list item is visible and the widget has
+ # the focus.
+ if top_is_visible:
+ self._top_bar.original_widget.set_text(self._topBar_endExposed_markup)
+ self._top_bar.set_attr_map(self._topBar_endExposed_focus
+ if focus else self._topBar_endExposed_offFocus)
+ else:
+ self._top_bar.original_widget.set_text(self._topBar_endCovered_markup.format(covered_above))
+ self._top_bar.set_attr_map(self._topBar_endCovered_focus
+ if focus else self._topBar_endCovered_offFocus)
+
+ # Changes the appearance of the bar at the bottom depending on whether the last list item is visible and the widget
+ # has the focus.
+ if bottom_is_visible:
+ self._bottom_bar.original_widget.set_text(self._bottomBar_endExposed_markup)
+ self._bottom_bar.set_attr_map(self._bottomBar_endExposed_focus
+ if focus else self._bottomBar_endExposed_offFocus)
+ else:
+ self._bottom_bar.original_widget.set_text(self._bottomBar_endCovered_markup.format(covered_below))
+ self._bottom_bar.set_attr_map(self._bottomBar_endCovered_focus
+ if focus else self._bottomBar_endCovered_offFocus)
+
+ # The highlighting in urwid is bound to the focus. This means that the selected item is only distinguishable as long as
+ # the widget has the focus. To compensate this, the color scheme of the selected item is otherwiese temporarily changed.
+ if focus and not self._last_focus_state and (self._original_item_attr_map is not None):
+ # Resets the appearance of the selected item to its original value.
+ self._listbox.focus.set_attr_map(self._original_item_attr_map)
+
+ elif (self._highlight_offFocus is not None) \
+ and not focus \
+ and (self._last_focus_state or (self._last_focus_state is None)) \
+ and not self.body_is_empty():
+ # Store the 'attr_map' of the selected item and then change it to accomplish off focus highlighting.
+ self._original_item_attr_map = self._listbox.focus.get_attr_map()
+ self._listbox.focus.set_attr_map(self._highlight_offFocus)
+
+ # Store the last focus to do/undo the off focus highlighting only if the focus has really changed and not if the
+ # widget is re-rendered because the terminal size has changed or similar.
+ self._last_focus_state = focus
+
+ return super().render(size, focus=focus)
+
+ def keypress(self, size, key):
+ just_maxcol = (size[0],)
+
+ # The size also includes the two bars, so subtract these.
+ modified_size = (size[0], # cols
+ size[1] - self._top_bar.rows(just_maxcol) - self._bottom_bar.rows(just_maxcol)) # rows
+
+ # Store the focus position before passing the keystroke to the contained list box. That way, it can be compared with the
+ # position after the input is processed. If the list box body is empty, store None.
+ focus_position_before_input = self.get_selected_position()
+
+ # A keystroke is changed to a modified one ('up' => 'ctrl up'). This prevents the widget from responding when the arrows
+ # keys are used to navigate between widgets. That way it can be used in a 'urwid.Pile' or similar.
+ if key == self._modifier_key.prepend_to("up"):
+ key = self._pass_key_to_contained_listbox(modified_size, "up")
+
+ elif key == self._modifier_key.prepend_to("down"):
+ key = self._pass_key_to_contained_listbox(modified_size, "down")
+
+ elif key == self._modifier_key.prepend_to("page up"):
+ key = self._pass_key_to_contained_listbox(modified_size, "page up")
+
+ elif key == self._modifier_key.prepend_to("page down"):
+ key = self._pass_key_to_contained_listbox(modified_size, "page down")
+
+ elif key == self._modifier_key.prepend_to("home"):
+ # Check if the first list item is already selected.
+ if (focus_position_before_input is not None) and (focus_position_before_input != 0):
+ self.select_first_item()
+ key = None
+ elif not self._return_unused_navigation_input:
+ key = None
+
+ elif key == self._modifier_key.prepend_to("end"):
+ # Check if the last list item is already selected.
+ if (focus_position_before_input is not None) and (focus_position_before_input != self.rearmost_position()):
+ self.select_last_item()
+ key = None
+ elif not self._return_unused_navigation_input:
+ key = None
+
+ elif key not in ("up", "down", "page up", "page down", "home", "end"):
+ key = self._listbox.keypress(modified_size, key)
+
+ focus_position_after_input = self.get_selected_position()
+
+ # If the focus position has changed, execute the hook (if existing).
+ if (focus_position_before_input != focus_position_after_input) and (self.on_selection_change is not None):
+ self.on_selection_change(focus_position_before_input,
+ focus_position_after_input)
+
+ return key
+
+ def mouse_event(self, size, event, button, col, row, focus):
+ just_maxcol = (size[0],)
+
+ topBar_rows = self._top_bar.rows(just_maxcol)
+ bottomBar_rows = self._bottom_bar.rows(just_maxcol)
+
+ # The size also includes the two bars, so subtract these.
+ modified_size = (size[0], # cols
+ size[1] - topBar_rows - bottomBar_rows) # rows
+
+ was_handeled = False
+
+ # An event is changed to a modified one ('mouse press' => 'ctrl mouse press'). This prevents the widget from responding
+ # when mouse buttons are also used to navigate between widgets.
+ if event == self._modifier_key.prepend_to("mouse press"):
+ # Store the focus position before passing the input to the contained list box. That way, it can be compared with the
+ # position after the input is processed. If the list box body is empty, store None.
+ focus_position_before_input = self.get_selected_position()
+
+ # left mouse button, if not top bar or bottom bar.
+ if (button == 1.0) and (topBar_rows <= row < (size[1] - bottomBar_rows)):
+ # Because 'row' includes the top bar, the offset must be substracted before passing it to the contained list box.
+ result = self._listbox.mouse_event(modified_size, event, button, col, (row - topBar_rows), focus)
+ was_handeled = result if self._return_unused_navigation_input else True
+
+ # mousewheel up
+ elif button == 4.0:
+ was_handeled = self._pass_key_to_contained_listbox(modified_size, "page up")
+
+ # mousewheel down
+ elif button == 5.0:
+ was_handeled = self._pass_key_to_contained_listbox(modified_size, "page down")
+
+ focus_position_after_input = self.get_selected_position()
+
+ # If the focus position has changed, execute the hook (if existing).
+ if (focus_position_before_input != focus_position_after_input) and (self.on_selection_change is not None):
+ self.on_selection_change(focus_position_before_input,
+ focus_position_after_input)
+
+ return was_handeled
+
+ # Pass the keystroke to the original widget. If it is not used, evaluate the corresponding variable to decide if it gets
+ # swallowed or not.
+ def _pass_key_to_contained_listbox(self, size, key):
+ result = self._listbox.keypress(size, key)
+ return result if self._return_unused_navigation_input else None
+
+ def get_body(self):
+ return self._listbox.body
+
+ def body_len(self):
+ return len(self.get_body())
+
+ def rearmost_position(self):
+ return len(self.get_body()) - 1 # last valid index
+
+ def body_is_empty(self):
+ return self.body_len() == 0
+
+ def position_is_valid(self, position):
+ return (position < self.body_len()) and (position >= 0)
+
+ # If the passed position is valid, it is returned. Otherwise, the nearest valid position is returned. This ensures that
+ # positions which are out of range do not result in an error.
+ def _get_nearest_valid_position(self, position):
+ if self.body_is_empty():
+ return None
+
+ pos_type = type(position)
+
+ if pos_type == int:
+ if self.position_is_valid(position):
+ return position
+
+ elif position < 0:
+ return 0
+
+ else:
+ return self.rearmost_position()
+
+ elif pos_type == self.__class__.POSITION:
+ if position == self.__class__.POSITION.LAST:
+ return self.rearmost_position()
+
+ elif position == self.__class__.POSITION.MIDDLE:
+ return self.body_len() // 2
+
+ elif position == self.__class__.POSITION.RANDOM:
+ return random.randint(0, self.rearmost_position())
+
+ else:
+ raise ValueError(self.__class__._VALUE_ERR_MSG.format(position))
+
+ else:
+ raise TypeError(self.__class__._TYPE_ERR_MSG.format("<class 'int'> or <enum 'IndicativeListBox.POSITION'>",
+ "'position'",
+ pos_type))
+
+ def get_item(self, position):
+ if self.position_is_valid(position):
+ body = self.get_body()
+ return body[position]
+ else:
+ return None
+
+ def get_first_item(self):
+ return self.get_item(0)
+
+ def get_last_item(self):
+ return self.get_item(self.rearmost_position())
+
+ def get_selected_item(self):
+ return self._listbox.focus
+
+ def get_selected_position(self):
+ return self._listbox.focus_position if not self.body_is_empty() else None
+
+ def first_item_is_selected(self):
+ return self.get_selected_position() == 0
+
+ def last_item_is_selected(self):
+ return self.get_selected_position() == self.rearmost_position()
+
+ def _reset_highlighting(self):
+ # Resets the appearance of the selected item to its original value, if off focus highlighting is active.
+ if not self._last_focus_state and (self._original_item_attr_map is not None):
+ self._listbox.focus.set_attr_map(self._original_item_attr_map)
+
+ # The next time the widget is rendered, the highlighting is redone.
+ self._original_item_attr_map = None
+ self._last_focus_state = None
+
+ def set_body(self, body, *, alternative_position=None):
+ focus_position_before_change = self.get_selected_position()
+
+ self._reset_highlighting()
+
+ # Wrap each item in an 'urwid.AttrMap', if not already done.
+ self._listbox.body[:] = [urwid.AttrMap(item, None) if not isinstance(item, urwid.AttrMap) else item
+ for item in body]
+
+ # Normally it is tried to hold the focus position. If this is not desired, a position can be passed.
+ if alternative_position is not None:
+ nearest_valid_position = self._get_nearest_valid_position(alternative_position)
+
+ if nearest_valid_position is not None:
+ # Because the widget has been re-rendered, the off focus highlighted item must be restored to its original state.
+ self._reset_highlighting()
+
+ self._listbox.set_focus(nearest_valid_position)
+
+ # If an initialization is considered a selection change, execute the hook (if existing).
+ if self._initialization_is_selection_change and (self.on_selection_change is not None):
+ self.on_selection_change(focus_position_before_change,
+ self.get_selected_position())
+
+ def select_item(self, position):
+ focus_position_before_change = self.get_selected_position()
+
+ nearest_valid_position = self._get_nearest_valid_position(position)
+
+ # Focus the new position, if possible and not already focused.
+ if (nearest_valid_position is not None) and (nearest_valid_position != focus_position_before_change):
+ self._reset_highlighting()
+
+ self._listbox.set_focus(nearest_valid_position)
+
+ # Execute the hook (if existing).
+ if (self.on_selection_change is not None):
+ self.on_selection_change(focus_position_before_change,
+ nearest_valid_position)
+
+ def select_first_item(self):
+ self.select_item(0)
+
+ def select_last_item(self):
+ self.select_item(self.rearmost_position())
+
+ def delete_position(self, position):
+ # The saved properties get reseted, just in case that the appearance of the items differs.
+ self._reset_highlighting()
+
+ body = self.get_body()
+ del body[position]
+
+ def delete_selected_position(self):
+ pos = self.get_selected_position()
+
+ # If the list body is not empty, delete the selected item.
+ if pos is not None:
+ self.delete_position(pos)
diff --git a/nomadnet/vendor/additional_urwid_widgets/widgets/integer_picker.py b/nomadnet/vendor/additional_urwid_widgets/widgets/integer_picker.py
new file mode 100644
index 0000000..b52ad6f
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/widgets/integer_picker.py
@@ -0,0 +1,277 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+from ..assisting_modules.modifier_key import MODIFIER_KEY # pylint: disable=unused-import
+from .selectable_row import SelectableRow
+
+import sys # pylint: disable=unused-import
+import urwid
+
+
+class IntegerPicker(urwid.WidgetWrap):
+ """Serves as a selector for integer numbers."""
+
+ def __init__(self, value, *, min_v=(-sys.maxsize - 1), max_v=sys.maxsize, step_len=1, jump_len=100, on_selection_change=None,
+ initialization_is_selection_change=False, modifier_key=MODIFIER_KEY.NONE, ascending=True,
+ return_unused_navigation_input=True, topBar_align="center", topBar_endCovered_prop=("▲", None, None),
+ topBar_endExposed_prop=("───", None, None), bottomBar_align="center", bottomBar_endCovered_prop=("▼", None, None),
+ bottomBar_endExposed_prop=("───", None, None), display_syntax="{}", display_align="center", display_prop=(None, None)):
+ assert (min_v <= max_v), "'min_v' must be less than or equal to 'max_v'."
+
+ assert (min_v <= value <= max_v), "'min_v <= value <= max_v' must be True."
+
+ self._value = value
+
+ self._minimum = min_v
+ self._maximum = max_v
+
+ # Specifies how far to move in the respective direction when the keys 'up/down' are pressed.
+ self._step_len = step_len
+
+ # Specifies how far to jump in the respective direction when the keys 'page up/down' or the mouse events 'wheel up/down'
+ # are passed.
+ self._jump_len = jump_len
+
+ # A hook which is triggered when the value changes.
+ self.on_selection_change = on_selection_change
+
+ # 'MODIFIER_KEY' changes the behavior, so that the widget responds only to modified input. ('up' => 'ctrl up')
+ self._modifier_key = modifier_key
+
+ # Specifies whether moving upwards represents a decrease or an increase of the value.
+ self._ascending = ascending
+
+ # If the minimum has been reached and an attempt is made to select an even smaller value, the input is normally not
+ # swallowed by the widget, but passed on so that other widgets can interpret it. This may result in transferring the focus.
+ self._return_unused_navigation_input = return_unused_navigation_input
+
+ # The bars are just 'urwid.Text' widgets.
+ self._top_bar = urwid.AttrMap(urwid.Text("", topBar_align),
+ None)
+
+ self._bottom_bar = urwid.AttrMap(urwid.Text("", bottomBar_align),
+ None)
+
+ # During the initialization of 'urwid.AttrMap', the value can be passed as non-dict. After initializing, its value can be
+ # manipulated by passing a dict. The dicts I create below will be used later to change the appearance of the widgets.
+ self._topBar_endCovered_markup = topBar_endCovered_prop[0]
+ self._topBar_endCovered_focus = {None:topBar_endCovered_prop[1]}
+ self._topBar_endCovered_offFocus = {None:topBar_endCovered_prop[2]}
+
+ self._topBar_endExposed_markup = topBar_endExposed_prop[0]
+ self._topBar_endExposed_focus = {None:topBar_endExposed_prop[1]}
+ self._topBar_endExposed_offFocus = {None:topBar_endExposed_prop[2]}
+
+ self._bottomBar_endCovered_markup = bottomBar_endCovered_prop[0]
+ self._bottomBar_endCovered_focus = {None:bottomBar_endCovered_prop[1]}
+ self._bottomBar_endCovered_offFocus = {None:bottomBar_endCovered_prop[2]}
+
+ self._bottomBar_endExposed_markup = bottomBar_endExposed_prop[0]
+ self._bottomBar_endExposed_focus = {None:bottomBar_endExposed_prop[1]}
+ self._bottomBar_endExposed_offFocus = {None:bottomBar_endExposed_prop[2]}
+
+ # Format the number before displaying it. That way it is easier to read.
+ self._display_syntax = display_syntax
+
+ # The current value is displayed via this widget.
+ self._display = SelectableRow([display_syntax.format(value)],
+ align=display_align)
+
+ display_attr = urwid.AttrMap(self._display,
+ display_prop[1],
+ display_prop[0])
+
+ # wrap 'urwid.Pile'
+ super().__init__(urwid.Pile([self._top_bar,
+ display_attr,
+ self._bottom_bar]))
+
+ # Is 'on_selection_change' triggered during the initialization?
+ if initialization_is_selection_change and (on_selection_change is not None):
+ on_selection_change(None, value)
+
+ def __repr__(self):
+ return "{}(value='{}', min_v='{}', max_v='{}', ascending='{}')".format(self.__class__.__name__,
+ self._value,
+ self._minimum,
+ self._maximum,
+ self._ascending)
+
+ def render(self, size, focus=False):
+ # Changes the appearance of the bar at the top depending on whether the upper limit is reached.
+ if self._value == (self._minimum if self._ascending else self._maximum):
+ self._top_bar.original_widget.set_text(self._topBar_endExposed_markup)
+ self._top_bar.set_attr_map(self._topBar_endExposed_focus
+ if focus else self._topBar_endExposed_offFocus)
+ else:
+ self._top_bar.original_widget.set_text(self._topBar_endCovered_markup)
+ self._top_bar.set_attr_map(self._topBar_endCovered_focus
+ if focus else self._topBar_endCovered_offFocus)
+
+ # Changes the appearance of the bar at the bottom depending on whether the lower limit is reached.
+ if self._value == (self._maximum if self._ascending else self._minimum):
+ self._bottom_bar.original_widget.set_text(self._bottomBar_endExposed_markup)
+ self._bottom_bar.set_attr_map(self._bottomBar_endExposed_focus
+ if focus else self._bottomBar_endExposed_offFocus)
+ else:
+ self._bottom_bar.original_widget.set_text(self._bottomBar_endCovered_markup)
+ self._bottom_bar.set_attr_map(self._bottomBar_endCovered_focus
+ if focus else self._bottomBar_endCovered_offFocus)
+
+ return super().render(size, focus=focus)
+
+ def keypress(self, size, key):
+ # A keystroke is changed to a modified one ('up' => 'ctrl up'). This prevents the widget from responding when the arrows
+ # keys are used to navigate between widgets. That way it can be used in a 'urwid.Pile' or similar.
+ if key == self._modifier_key.prepend_to("up"):
+ successful = self._change_value(-self._step_len)
+
+ elif key == self._modifier_key.prepend_to("down"):
+ successful = self._change_value(self._step_len)
+
+ elif key == self._modifier_key.prepend_to("page up"):
+ successful = self._change_value(-self._jump_len)
+
+ elif key == self._modifier_key.prepend_to("page down"):
+ successful = self._change_value(self._jump_len)
+
+ elif key == self._modifier_key.prepend_to("home"):
+ successful = self._change_value(float("-inf"))
+
+ elif key == self._modifier_key.prepend_to("end"):
+ successful = self._change_value(float("inf"))
+
+ else:
+ successful = False
+
+ return key if not successful else None
+
+ def mouse_event(self, size, event, button, col, row, focus):
+ if focus:
+ # An event is changed to a modified one ('mouse press' => 'ctrl mouse press'). This prevents the original widget from
+ # responding when mouse buttons are also used to navigate between widgets.
+ if event == self._modifier_key.prepend_to("mouse press"):
+ # mousewheel up
+ if button == 4.0:
+ result = self._change_value(-self._jump_len)
+ return result if self._return_unused_navigation_input else True
+
+ # mousewheel down
+ elif button == 5.0:
+ result = self._change_value(self._jump_len)
+ return result if self._return_unused_navigation_input else True
+
+ return False
+
+ # This method tries to change the value depending on the desired arrangement and returns True if this change was successful.
+ def _change_value(self, summand):
+ value_before_input = self._value
+
+ if self._ascending:
+ new_value = self._value + summand
+
+ if summand < 0:
+ # If the corresponding limit has already been reached, then determine whether the unused input should be
+ # returned or swallowed.
+ if self._value == self._minimum:
+ return not self._return_unused_navigation_input
+
+ # If the new value stays within the permitted range, use it.
+ elif new_value > self._minimum:
+ self._value = new_value
+
+ # The permitted range would be exceeded, so the limit is set instead.
+ else:
+ self._value = self._minimum
+
+ elif summand > 0:
+ if self._value == self._maximum:
+ return not self._return_unused_navigation_input
+
+ elif new_value < self._maximum:
+ self._value = new_value
+
+ else:
+ self._value = self._maximum
+ else:
+ new_value = self._value - summand
+
+ if summand < 0:
+ if self._value == self._maximum:
+ return not self._return_unused_navigation_input
+
+ elif new_value < self._maximum:
+ self._value = new_value
+
+ else:
+ self._value = self._maximum
+
+ elif summand > 0:
+ if self._value == self._minimum:
+ return not self._return_unused_navigation_input
+
+ elif new_value > self._minimum:
+ self._value = new_value
+
+ else:
+ self._value = self._minimum
+
+ # Update the displayed value.
+ self._display.set_contents([self._display_syntax.format(self._value)])
+
+ # If the value has changed, execute the hook (if existing).
+ if (value_before_input != self._value) and (self.on_selection_change is not None):
+ self.on_selection_change(value_before_input,
+ self._value)
+
+ return True
+
+ def get_value(self):
+ return self._value
+
+ def set_value(self, value):
+ if not (self._minimum <= value <= self._maximum):
+ raise ValueError("'minimum <= value <= maximum' must be True.")
+
+ if value != self._value:
+ value_before_change = self._value
+ self._value = value
+
+ # Update the displayed value.
+ self._display.set_contents([self._display_syntax.format(self._value)])
+
+ # Execute the hook (if existing).
+ if (self.on_selection_change is not None):
+ self.on_selection_change(value_before_change, self._value)
+
+ def set_to_minimum(self):
+ self.set_value(self._minimum)
+
+ def set_to_maximum(self):
+ self.set_value(self._maximum)
+
+ def set_minimum(self, new_min):
+ if new_min > self._maximum:
+ raise ValueError("'new_min' must be less than or equal to the maximum value.")
+
+ self._minimum = new_min
+
+ if self._value < new_min:
+ self.set_to_minimum()
+
+ def set_maximum(self, new_max):
+ if new_max < self._minimum:
+ raise ValueError("'new_max' must be greater than or equal to the minimum value.")
+
+ self._maximum = new_max
+
+ if self._value > new_max:
+ self.set_to_maximum()
+
+ def minimum_is_selected(self):
+ return self._value == self._minimum
+
+ def maximum_is_selected(self):
+ return self._value == self._maximum
+
\ No newline at end of file
diff --git a/nomadnet/vendor/additional_urwid_widgets/widgets/message_dialog.py b/nomadnet/vendor/additional_urwid_widgets/widgets/message_dialog.py
new file mode 100644
index 0000000..9100cd9
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/widgets/message_dialog.py
@@ -0,0 +1,40 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+import urwid
+
+
+class MessageDialog(urwid.WidgetWrap):
+ """Wraps 'urwid.Overlay' to show a message and expects a reaction from the user."""
+
+ def __init__(self, contents, btns, overlay_size, *, contents_align="left", space_between_btns=2, title="", title_align="center",
+ background=urwid.SolidFill("#"), overlay_align=("center", "middle"), overlay_min_size=(None, None), left=0, right=0,
+ top=0, bottom=0):
+ # Message part
+ texts = [urwid.Text(content, align=contents_align)
+ for content in contents]
+
+ # Lower part
+ lower_part = [urwid.Divider(" "),
+ urwid.Columns(btns, dividechars=space_between_btns)]
+
+ # frame
+ line_box = urwid.LineBox(urwid.Pile(texts + lower_part),
+ title=title,
+ title_align=title_align)
+
+ # Wrap 'urwid.Overlay'
+ super().__init__(urwid.Overlay(urwid.Filler(line_box),
+ background,
+ overlay_align[0],
+ overlay_size[0],
+ overlay_align[1],
+ overlay_size[1],
+ min_width=overlay_min_size[0],
+ min_height=overlay_min_size[1],
+ left=left,
+ right=right,
+ top=top,
+ bottom=bottom))
+
\ No newline at end of file
diff --git a/nomadnet/vendor/additional_urwid_widgets/widgets/selectable_row.py b/nomadnet/vendor/additional_urwid_widgets/widgets/selectable_row.py
new file mode 100644
index 0000000..d7fd644
--- /dev/null
+++ b/nomadnet/vendor/additional_urwid_widgets/widgets/selectable_row.py
@@ -0,0 +1,47 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+import urwid
+
+
+class SelectableRow(urwid.WidgetWrap):
+ """Wraps 'urwid.Columns' to make it selectable.
+ This class has been slightly modified, but essentially corresponds to this class posted on stackoverflow.com:
+ https://stackoverflow.com/questions/52106244/how-do-you-combine-multiple-tui-forms-to-write-more-complex-applications#answer-52174629"""
+
+ def __init__(self, contents, *, align="left", on_select=None, space_between=2):
+ # A list-like object, where each element represents the value of a column.
+ self.contents = contents
+
+ self._columns = urwid.Columns([urwid.Text(c, align=align) for c in contents],
+ dividechars=space_between)
+
+ # Wrap 'urwid.Columns'.
+ super().__init__(self._columns)
+
+ # A hook which defines the behavior that is executed when a specified key is pressed.
+ self.on_select = on_select
+
+ def __repr__(self):
+ return "{}(contents='{}')".format(self.__class__.__name__,
+ self.contents)
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ if (key == "enter") and (self.on_select is not None):
+ self.on_select(self)
+ key = None
+
+ return key
+
+ def set_contents(self, contents):
+ # Update the list record inplace...
+ self.contents[:] = contents
+
+ # ... and update the displayed items.
+ for t, (w, _) in zip(contents, self._columns.contents):
+ w.set_text(t)
+
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────